Ce tutorial reprend la course d'escargots du codelab preoo-03-static-classes et la transforme en code orienté objet avec des instances.
À la fin du codelab précédent, le code était organisé en classes statiques :
SnailRaceRefactoring/
├── Program.cs
├── Game/
│ ├── Race.cs // static class Race (données + logique)
│ └── Snail.cs // static class Snail (Draw/Clear)
└── Display/
└── SuperConsole.cs // static class SuperConsole
La classe Race contient des tableaux parallèles (SnailX[], SnailY[], SnailEnergy[], SnailNames[], SnailColors[]) pour gérer les escargots. Ça fonctionne, mais c'est fragile et difficile à étendre.
À travers 5 étapes progressives, vous apprendrez à :
Snail instanciable avec un constructeurSnail[]La classe Race contient 5 tableaux parallèles pour gérer 3 escargots :
static class Race
{
public static int NumberOfSnails = 3;
public static int[] SnailX = new int[3];
public static int[] SnailY = new int[3];
public static int[] SnailEnergy = new int[3];
public static string[] SnailNames = { "Turbo", "Speedy", "Flash" };
public static ConsoleColor[] SnailColors = {
ConsoleColor.Yellow, ConsoleColor.Cyan, ConsoleColor.Magenta
};
// ...
}
Pour accéder aux données de l'escargot i, il faut fouiller dans 5 tableaux :
// Tout ça concerne le MÊME escargot !
Race.SnailX[i]
Race.SnailY[i]
Race.SnailEnergy[i]
Race.SnailNames[i]
Race.SnailColors[i]
Données éclatées | 5 tableaux pour décrire un escargot |
Indices fragiles | Se tromper d'indice = bug silencieux |
Pas extensible | Ajouter un champ (vitesse max ?) = 1 tableau de plus |
Fonctions lourdes |
|
Chaque escargot devrait être un objet qui transporte ses propres données :
// Au lieu de 5 tableaux...
snails[i].Move(step, 0);
snails[i].Draw();
// L'escargot connaît sa position, sa couleur, son énergie !
Transformer Snail.cs : passer d'une classe statique avec juste Draw/Clear à une classe instanciable complète qui contient toutes les données d'un escargot.
Remplacer le contenu de Snail.cs par :
namespace SnailRaceRefactoring.Game
{
class Snail
{
// Champs : chaque instance a ses propres valeurs
public int X;
public int Y;
public int Energy;
public string Name;
public ConsoleColor Color;
// Constructeur : initialise un escargot
public Snail(string name, ConsoleColor color)
{
this.Name = name;
this.Color = color;
this.X = 0;
this.Y = 0;
this.Energy = 100;
}
}
}
|
|
Pas de données (juste Draw/Clear) | Champs |
Pas de constructeur | Constructeur |
Le constructeur reçoit les données qui identifient l'escargot (name, color). Les données de position (X, Y) et d'énergie (Energy) sont initialisées à des valeurs par défaut car elles seront configurées au moment de la course.
thisthis désigne l'instance en cours de création :
public Snail(string name, ConsoleColor color)
{
this.Name = name; // this.Name = champ de l'objet
// name = paramètre du constructeur
this.Color = color;
}
Dans Program.cs, on peut maintenant écrire (juste pour tester, on intégrera dans la course plus tard) :
Snail turbo = new Snail("Turbo", ConsoleColor.Yellow);
Snail speedy = new Snail("Speedy", ConsoleColor.Cyan);
Snail flash = new Snail("Flash", ConsoleColor.Magenta);
Console.WriteLine($"{turbo.Name} a {turbo.Energy} d'énergie");
Console.WriteLine($"{speedy.Name} a {speedy.Energy} d'énergie");
Console.WriteLine($"{flash.Name} a {flash.Energy} d'énergie");
Chaque escargot est indépendant : modifier turbo.Energy ne change pas speedy.Energy.
Ajouter les méthodes Draw, Erase et Move à la classe Snail. Ces méthodes utilisent les données de leur instance (via this).
using SnailRaceRefactoring.Display;
namespace SnailRaceRefactoring.Game
{
class Snail
{
// Champs
public int X;
public int Y;
public int Energy;
public string Name;
public ConsoleColor Color;
// Constructeur
public Snail(string name, ConsoleColor color)
{
this.Name = name;
this.Color = color;
this.X = 0;
this.Y = 0;
this.Energy = 100;
}
// Dessiner l'escargot à sa position
public void Draw()
{
SuperConsole.WriteAtColor(this.X, this.Y, "@", this.Color);
}
// Effacer l'escargot de sa position actuelle
public void Erase()
{
SuperConsole.WriteAtColor(this.X, this.Y, ".", ConsoleColor.DarkGray);
}
// Déplacer l'escargot
public void Move(int dx, int dy)
{
this.X = this.X + dx;
this.Y = this.Y + dy;
}
// Réduire l'énergie
public void ReduceEnergy(int amount)
{
this.Energy = this.Energy - amount;
if (this.Energy < 10)
{
this.Energy = 10;
}
}
// Afficher la barre d'énergie
public void DrawEnergy(int displayX, int displayY)
{
Console.SetCursorPosition(displayX, displayY);
Console.ForegroundColor = this.Color;
string bar = new string('#', this.Energy / 10);
string empty = new string('-', 10 - this.Energy / 10);
Console.Write($"{this.Name,6}[{bar}{empty}] ");
Console.ResetColor();
}
}
}
| Dessine l'escargot |
|
| Efface l'escargot |
|
| Déplace l'escargot |
|
| Réduit l'énergie (min 10) |
|
| Affiche la barre d'énergie |
|
|
|
|
|
|
|
|
|
Remplacer les tableaux parallèles dans Race par un tableau d'objetsSnail[].
namespace SnailRaceRefactoring.Game
{
static class Race
{
// Configuration de la piste
public static int ScreenWidth = 50;
public static int StartX = 2;
public static int FinishX = ScreenWidth - 3;
// Tableau d'objets (remplace les 5 tableaux parallèles !)
public static Snail[] Snails = Array.Empty<Snail>();
public static void Init()
{
// Créer les escargots
Snails = new Snail[]
{
new Snail("Turbo", ConsoleColor.Yellow),
new Snail("Speedy", ConsoleColor.Cyan),
new Snail("Flash", ConsoleColor.Magenta)
};
// Positionner chaque escargot sur la piste
for (int i = 0; i < Snails.Length; i++)
{
Snails[i].X = StartX;
Snails[i].Y = 2 + i * 2;
Snails[i].Energy = 100;
}
}
public static void DrawTrack()
{
for (int i = 0; i < Snails.Length; i++)
{
int y = 2 + i * 2;
Console.SetCursorPosition(StartX - 1, y);
Console.ForegroundColor = ConsoleColor.White;
Console.Write("|");
for (int x = StartX; x < FinishX; x++)
{
Console.SetCursorPosition(x, y);
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.Write(".");
}
Console.SetCursorPosition(FinishX, y);
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("|");
}
Console.ResetColor();
}
public static void DrawAllSnails()
{
foreach (Snail snail in Snails)
{
snail.Draw();
}
}
public static void ShowEnergy()
{
for (int i = 0; i < Snails.Length; i++)
{
Snails[i].DrawEnergy(0, 10 + i);
}
}
}
}
| Supprimé (dans chaque |
| Supprimé (dans chaque |
| Supprimé (dans chaque |
| Supprimé (dans chaque |
| Supprimé (dans chaque |
|
|
— |
|
Observez la méthode DrawAllSnails :
// Avant : il fallait passer toutes les données en paramètres
Snail.Draw(SnailX[i], SnailY[i], SnailColors[i]);
// Après : l'escargot sait tout de lui-même
snail.Draw();
Le foreach est possible car chaque Snail est un objet complet.
C'est trivial — il suffit d'ajouter une ligne dans Init() :
Snails = new Snail[]
{
new Snail("Turbo", ConsoleColor.Yellow),
new Snail("Speedy", ConsoleColor.Cyan),
new Snail("Flash", ConsoleColor.Magenta),
new Snail("Rocket", ConsoleColor.Red) // Nouveau !
};
Aucun autre changement nécessaire : Snails.Length s'adapte automatiquement, les boucles foreach fonctionnent.
Réécrire la boucle de course dans Program.cs en utilisant les instances.
using SnailRaceRefactoring.Game;
using SnailRaceRefactoring.Display;
// Initialisation
Console.Clear();
Console.CursorVisible = false;
Race.Init();
Race.DrawTrack();
Race.DrawAllSnails();
SuperConsole.WriteAt(0, 12, "Appuyez sur ENTER pour lancer la course !");
Console.ReadLine();
// Boucle de course
Random random = new Random();
bool raceFinished = false;
Snail? winner = null;
while (!raceFinished)
{
foreach (Snail snail in Race.Snails)
{
// Tenter d'avancer selon l'énergie
if (random.Next(100) < snail.Energy)
{
snail.Erase();
snail.Move(1, 0);
snail.Draw();
snail.ReduceEnergy(random.Next(1, 4));
}
// Vérifier si cet escargot a franchi la ligne
if (snail.X >= Race.FinishX)
{
winner = snail;
raceFinished = true;
break;
}
}
Race.ShowEnergy();
Thread.Sleep(100);
}
// Afficher le gagnant
Console.SetCursorPosition(0, 14);
SuperConsole.WriteLineColor($"{winner!.Name} a gagné la course !", winner.Color);
Console.ReadKey();
Avant (tableaux parallèles) :
for (int i = 0; i < Race.NumberOfSnails; i++)
{
if (random.Next(100) < Race.SnailEnergy[i])
{
Snail.Clear(Race.SnailX[i], Race.SnailY[i]);
Race.SnailX[i]++;
Snail.Draw(Race.SnailX[i], Race.SnailY[i], Race.SnailColors[i]);
Race.SnailEnergy[i] -= random.Next(1, 4);
if (Race.SnailEnergy[i] < 10) Race.SnailEnergy[i] = 10;
}
if (Race.SnailX[i] >= Race.FinishX)
{
winner = i;
// ...
}
}
Après (instances) :
foreach (Snail snail in Race.Snails)
{
if (random.Next(100) < snail.Energy)
{
snail.Erase();
snail.Move(1, 0);
snail.Draw();
snail.ReduceEnergy(random.Next(1, 4));
}
if (snail.X >= Race.FinishX)
{
winner = snail;
// ...
}
}
Variable gagnant |
|
|
Accès au nom du gagnant |
|
|
Boucle |
|
|
Déplacement | 3 lignes avec indices |
|
Dessin |
|
|
Compiler et exécuter. La course doit fonctionner exactement comme avant !
SnailRaceRefactoring/
├── Program.cs // Boucle de course simplifiée
├── Game/
│ ├── Race.cs // Piste + tableau Snail[]
│ └── Snail.cs // Classe instanciable complète
└── Display/
└── SuperConsole.cs // Utilitaires console (inchangé)
Comprendre que les variables d'objet contiennent des références, pas des copies — et les conséquences dans le code de la course.
null dans la courseDans Program.cs, on a écrit :
Snail? winner = null;
Le ? après Snail indique que la variable peut valoir null (ne pointer vers aucun objet). Au départ, on ne connaît pas le gagnant, donc winner ne pointe vers rien.
À la fin de la course, winner pointe vers l'escargot gagnant :
winner = snail; // winner pointe maintenant vers le même objet que snail
Et on peut écrire :
SuperConsole.WriteLineColor($"{winner!.Name} a gagné !", winner.Color);
Le ! après winner dit au compilateur : "je sais que ce n'est pas null ici".
Ajouter ce code temporaire dans Program.cs après Race.Init() pour observer le comportement des références :
Snail a = Race.Snails[0];
Snail b = Race.Snails[0]; // a et b pointent vers le MÊME objet
b.X = 99;
Console.WriteLine(a.X); // Affiche 99 ! (même objet)
C'est exactement ce qui se passe dans la boucle de course : quand on écrit snail.Move(1, 0) dans le foreach, la variable snail et Race.Snails[i] pointent vers le même objet. La modification est visible partout.
Créer une fonction utilitaire pour tester :
void Boost(Snail s)
{
s.Energy = 100;
}
// Utilisation
Boost(Race.Snails[0]); // Remet l'énergie de Turbo à 100
Console.WriteLine(Race.Snails[0].Energy); // Affiche 100
La fonction reçoit une référence vers l'escargot : elle modifie l'original, pas une copie. C'est très différent d'un int :
void DoubleScore(int score)
{
score = score * 2; // Modifie une COPIE locale
}
int points = 50;
DoubleScore(points);
Console.WriteLine(points); // Affiche 50 (inchangé !)
| Copie la valeur | Copie (indépendante) |
|
| Copie la référence (même objet) | Référence (même objet) |
|
Chaque escargot peut avoir une vitesse différente :
class Snail
{
// ... champs existants ...
public int MaxSpeed;
public Snail(string name, ConsoleColor color, int maxSpeed = 1)
{
// ... initialisations existantes ...
this.MaxSpeed = maxSpeed;
}
}
Dans Race.Init() :
Snails = new Snail[]
{
new Snail("Turbo", ConsoleColor.Yellow, 2), // Rapide
new Snail("Speedy", ConsoleColor.Cyan, 1), // Normal
new Snail("Flash", ConsoleColor.Magenta, 3) // Très rapide
};
Dans la boucle de course :
int step = random.Next(1, snail.MaxSpeed + 1);
snail.Move(step, 0);
Au lieu de @ pour tous, chaque escargot affiche son propre symbole :
class Snail
{
// ... champs existants ...
public string Symbol;
public Snail(string name, ConsoleColor color, string symbol = "@")
{
// ...
this.Symbol = symbol;
}
public void Draw()
{
SuperConsole.WriteAtColor(this.X, this.Y, this.Symbol, this.Color);
}
public void Erase()
{
SuperConsole.WriteAtColor(this.X, this.Y, ".", ConsoleColor.DarkGray);
}
}
Utilisation :
Snails = new Snail[]
{
new Snail("Turbo", ConsoleColor.Yellow) { Symbol = "🐌" },
new Snail("Speedy", ConsoleColor.Cyan) { Symbol = "🐢" },
new Snail("Flash", ConsoleColor.Magenta) { Symbol = "🐇" }
};
Ajouter une méthode pour afficher les infos de l'escargot :
public override string ToString()
{
return $"{this.Name} (x={this.X}, y={this.Y}, energy={this.Energy})";
}
Utilisation :
Console.WriteLine(turbo); // Affiche : Turbo (x=5, y=2, energy=87)
Puisque Race reste statique pour l'instant, essayer de lancer plusieurs courses successives en réinitialisant avec Race.Init(). Observer que les escargots sont recréés à chaque fois.
Données escargot | 5 tableaux parallèles | Champs dans chaque objet |
Création | Remplir 5 tableaux à l'indice |
|
Accès |
|
|
Dessin |
|
|
Ajout d'un escargot | Modifier 5 tableaux + | Ajouter un |
Ajout d'un champ | Créer un 6e tableau | Ajouter un champ à la classe |
Boucle |
|
|
class (sans static) = modèle pour créer des objetsnew = créer une instance avec ses propres donnéesthis = désigne l'instance courantePour l'instant, tous les champs de Snail sont public : n'importe quel code peut écrire snail.Energy = 9999. La prochaine étape introduira private et les propriétés pour protéger les données.